#!/usr/bin/env python
# -*- coding:utf-8 -*-
import codecs
import glob
import hashlib
import os
import re
import select
import shutil
import subprocess
import sys
import time
import traceback
from datetime import datetime


#
# LOGGING_DATE_FORMAT = '%H:%M:%S'
# LOGGING_MSG_FORMAT = '[%(asctime)s]%(message)s'
# logging.basicConfig(level=logging.INFO, format=LOGGING_MSG_FORMAT, datefmt=LOGGING_DATE_FORMAT)
#
# logger = logging.getLogger("builder")

def no_line_end(string):
    return string.rstrip('\r\n')


def str_iter(string_or_list, fail=True):
    """
    统一为一个可以遍历的结构
    :param string_or_list:
    :param fail:
    :return:
    """
    if string_or_list is None:
        return ""
    t = type(string_or_list)
    if t is str:
        string = string_or_list.splitlines(False)
    elif t is list:
        string = string_or_list
    elif t is iter:
        string = string_or_list
    elif t is type((x for x in xrange(1))):
        string = string_or_list
    else:
        if fail:
            Fail("不能处理的类型[%s]" % t, level=1)
            return
        else:
            return None
    return string


def _now():
    return int(time.time() * 1000)


def _msg(msg, prefix=None):
    if type(msg) is str:
        if prefix is None or len(prefix) == 0:
            return msg.splitlines()
        else:
            return map(lambda x: '%s %s' % (prefix, x), msg.splitlines())
    elif type(msg) is list:
        if prefix is None or len(prefix) == 0:
            return msg
        else:
            return map(lambda x: '%s %s' % (prefix, x), msg)
    else:
        raise Exception("不支持的类型[%s]" % type(msg))


_last_newline = False
_log_start = None

__log_init = False
__color_log = False


def _log(prefix, msg, color=None, newline=True, out=sys.stdout):
    if color is not None:
        global __log_init, __color_log
        if not __log_init:
            __log_init = True
            try:
                from colorama import init
                init(autoreset=True)
                __color_log = True
            finally:
                pass
        if __color_log:
            from colorama import Fore
            color = Fore.__dict__[str(color).upper()]
        else:
            color = None
    if msg is None or len(msg) == 0:
        return
    global _last_newline, _log_start
    if not _last_newline:
        if newline:
            # 防止狗尾续貂式的log
            print
    _last_newline = newline
    if _log_start is None:
        time_str = time.strftime('%H:%M:%S.') + datetime.utcnow().strftime('%f')[:3]
    else:
        time_str = time.strftime('%H:%M:%S.', time.gmtime(time.time() - _log_start)) + \
                   datetime.utcnow().strftime('%f')[:3]
    if prefix is not None:
        msg = '[' + time_str + ']' + '[' + prefix + '] ' + msg
    if color is not None:
        msg = color + msg
    if newline:
        out.write(msg)
        if msg[-1] != '\n':
            out.write('\n')
    else:
        out.write(msg)


def Never():
    """
    用来标记永远不应该到达的逻辑分支或语句块
    :return:
    """
    Fail("不应该到达这里的", level=1)


class FailError(RuntimeError):
    def __init__(self):
        RuntimeError.__init__(self)


def Fail(msg, level=0):
    stack = traceback.extract_stack()[-2 - level]
    line = stack[1]
    func = stack[2]
    prefix = 'ERROR@%s:%d' % (func, line)
    for line in _msg(msg):
        _log(prefix, line, color="red")
    raise FailError()


def error(msg):
    for line in _msg(msg):
        _log('ERROR', line, color="red")


def fatal(msg):
    for line in _msg(msg):
        _log('FATAL', line, color="red", out=sys.stderr)


def log_start(start=None):
    """
    log中的时间将按照与此时间的时间差显示
    :param start: None则表示为按系统时间显示
    """
    global _log_start
    _log_start = start


def wait(char='.'):
    _log(None, char, newline=False)


def info(msg, newline=True):
    for line in _msg(msg):
        _log('INFO_', line, color="white", newline=newline)


def debug(msg, newline=True):
    for line in _msg(msg):
        _log('DEBUG', line, newline=newline)


def warn(msg, newline=True):
    for line in _msg(msg):
        _log('WARN_', line, color="yellow", newline=newline)


def Assert(msg, expr=False, fail_level=0):
    if expr:
        return True
    Fail(msg, level=1 + fail_level)


def AssertNotEmpty(msg, expr, fail_level=0):
    if expr is None or len(expr) == 0:
        Fail(msg, level=1 + fail_level)


def AssertNotNull(msg, expr=None, fail_level=0):
    if expr is None:
        Fail(msg, level=1 + fail_level)


def AssertNull(msg, expr=None, fail_level=0):
    if expr is None:
        return
    Fail(msg, level=1 + fail_level)


def env(key, default=None, fail=True):
    if key in os.environ:
        return str(os.environ[key]).strip()
    else:
        if fail:
            raise Exception("环境变量[%s]没有找到" % key)
        else:
            return default


def checkfile(path):
    if not os.path.exists(path):
        return None
    m2 = hashlib.md5()
    with open(path, mode='rb') as fin:
        m2.update(fin.read())
    return m2.hexdigest()


def file_txt(path):
    out = ""
    for line in file_line_iter(path):
        out += line + "\n"
    return out


def file_props(path):
    out = {}
    for line in file_line_iter(path):
        line = line.lstrip()
        if line.startswith('#'):
            continue
        i = line.index('=')
        key = line[:i].strip()
        value = line[i + 1:].strip()

        # def inject(src, key, value):
        #     src[key] = value
        #     _ = key.find('.')
        #     if _ > 0:
        #         key2 = key[:_]
        #         if key2 not in src:
        #             src[key2] = {}
        #         inject(src[key2], key[_ + 1:], value)
        #
        # inject(out, key, value)
        out[key] = value
    return out


def head(string_or_list, line_num=1, fail=True, start=None, fail_level=0, found=False):
    string = str_iter(string_or_list)
    if string is None:
        if fail:
            Fail("未知错误", level=1 + fail_level)
        return None
    num = line_num
    buff = []
    if start is None or start == 0:
        for line in string:
            buff.append(line)
            num -= 1
            if num == 0:
                break
    else:
        if type(start) is int:
            for line in string:
                if start > 0:
                    start -= 1
                    continue
                buff.append(line)
                num -= 1
                if num == 0:
                    break
        elif type(start) is str:
            start = re.compile(start)
            is_start = False
            for line in string:
                if not is_start:
                    if start.findall(line):
                        is_start = True
                    else:
                        continue
                buff.append(line)
                num -= 1
                if num == 0:
                    break
        else:
            Fail("start条件[%s]不可用" % start, level=1 + fail_level)
            return
    l = len(buff)
    if l < line_num and found:
        if l == 0:
            if start is None:
                Fail("没有内容", level=1 + fail_level)
            else:
                if type(start) is int:
                    Fail("内容行数不够[%s]" % start, level=1 + fail_level)
                elif type(start) is str:
                    Fail("没有找到[%s]" % start, level=1 + fail_level)
        else:
            Fail("内容不足", level=1 + fail_level)
        return

    if len(buff) == 0:
        return ""
    if line_num == 1:
        return buff[0]
    else:
        return buff


def file_head(path, line_num=1, fail=True):
    if line_num <= 0:
        return ""
    return head(file_line_iter(path, keepLineEnd=False), fail=fail, fail_level=1, line_num=line_num)


def wildone(expr, pwd=None, fail=True):
    if pwd is not None and not expr.startswith('/'):
        expr = os.path.join(pwd, expr)
    files = glob.glob(expr)
    if len(files) == 0 and fail:
        Fail("没有找到文件[%s]" % expr, level=1)
    if len(files) == 1:
        return files[0]
    if fail:
        Fail('不只一个文件[%s][%s]' % (files[0], files[1]), level=1)
    else:
        return None


def cpfile(from_path, to_path, remove=False):
    if checkfile(from_path) == checkfile(to_path):
        return
    name = os.name
    if remove:
        if name == 'posix':
            os.system('mv "%s" "%s"' % (from_path, to_path))
        elif name == 'nt':
            os.system('move "%s" "%s"' % (from_path, to_path))
        else:
            shutil.move(from_path, to_path)
    else:
        if name == 'posix':
            os.system('cp "%s" "%s"' % (from_path, to_path))
        elif name == 'nt':
            os.system('copy "%s" "%s"' % (from_path, to_path))
        else:
            open(to_path, mode='wb').write(open(from_path, 'rb').read())


def replace_in_file(path, expr, to, found=False):
    to = to.decode('utf8')
    expr = re.compile(expr)
    with codecs.open(path, mode='r', encoding='utf8') as fin:
        lines = fin.readlines()
    output = []
    num = 0
    for line in lines:
        cur = 0
        nl = ''
        for item in expr.finditer(line):
            nl += line[cur:item.start()]
            nl += to
            cur = item.end()
            num += 1

        nl += line[cur:]
        output.append(nl)
    if num > 0:
        with codecs.open(path, mode='w', encoding='utf8') as fout:
            for line in output:
                fout.write(line)
        return True
    else:
        if found:
            Fail("文件[%s]中没有找到匹配项" % path, level=1)
        return False


def AssertMac(mav_ver=None):
    Assert("系统不是Mac", os.name == 'posix')
    import platform
    ret = platform.mac_ver()
    Assert("系统不是Mac", len(ret[0]) > 0)
    if mav_ver is not None and type(mav_ver) is str:
        mav_ver = mav_ver.split('.')
        ret = ret[0].split('.')
        Assert("系统版本不够", ret[0] >= mav_ver[0])


class ConsoleException(FailError):
    def __init__(self, msg, stdout, stderr):
        Exception.__init__(self, msg)

        def to_str(obj):
            if obj is None:
                return ""
            if type(obj) is str:
                return obj
            if type(obj) is list:
                return '\n'.join(map(no_line_end, obj))

        self.stdout = to_str(stdout)
        self.stderr = to_str(stderr)


def simple_run(cmd, pwd=None, fail=False, cmd_verbose=False, default=None):
    buff = []
    if run_cmd(cmd, fail=fail, pwd=pwd, cmd_verbose=cmd_verbose, verbose=False, output=True, stdout_buff=buff) is None:
        return default
    return '\n'.join(buff)


def run_cmd(cmd, fail=True, pwd=None, verbose=True, cmd_verbose=True, log=None, output=True, hide=None,
            timeout=600000, stdout_buff=None, stderr_buff=None):
    """
    执行一个命令
    :param cmd: 完整的命令行
    :param fail: 失败即异常
    :param pwd: 命令执行的当前目录
    :param verbose: 是否回显(不会显就打点表示执行时间)
    :param cmd_verbose: 是否显示打印执行的命令
    :param log: 额外的log文件(当做命令的执行回显处理)
    :param output: 将命令输出返回
    :param hide: 回显中隐藏的指定字符串(前后请用额外的一个'#'标记)
    :param timeout: 执行超时(默认10min)
    :param stdout_buff: 输出的缓存(如果需要的话)
    :param stderr_buff: 输出的缓存(如果需要的话)
    :return: 程序的返回码
    """
    if cmd_verbose:
        hint = cmd.lstrip()
        if hide:
            if type(hide) == str:
                hide = [hide]
            for i in hide:
                hint = hint.replace('#%s#' % i, "******")
                cmd = cmd.replace('#%s#' % i, i)
        PATH = env('PATH', default='.', fail=False)
        for p in PATH.split(os.path.pathsep):
            if hint.startswith(p):
                hint = hint[len(p) + 1:]
                break
        info('+ %s' % hint)
    p = subprocess.Popen(cmd, bufsize=1, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=pwd)
    start = _now()
    expire = start + 3000
    timeout_expire = start + timeout

    def timeout_check(t):
        if t > timeout_expire:
            try:
                p.kill()
            except:
                pass
            Fail("执行命令[%s]超时了" % cmd[:1000], level=2)

    # todo: 非阻塞读取stdout与stderr 保证顺序
    logfile = None
    if verbose:
        while p.poll() is None:
            now = _now()
            change = False
            if select.select([p.stdout], [], [], 0) == ([p.stdout], [], []):
                line = p.stdout.readline()
                if len(line):
                    change = True
                    if stdout_buff is not None:
                        stdout_buff.append(line)
                    debug('- %s' % line)
            if select.select([p.stderr], [], [], 0) == ([p.stderr], [], []):
                line = p.stderr.readline()
                if len(line):
                    change = True
                    if stderr_buff is not None:
                        stderr_buff.append(line)
                    error('- %s' % line)
            if logfile is None:
                if log is not None and os.path.exists(log):
                    logfile = open(log, mode='r')
            if logfile is not None:
                if select.select([logfile], [], [], 0) == ([logfile], [], []):
                    line = logfile.readline()
                    if len(line):
                        change = True
                        if stdout_buff is not None:
                            stdout_buff.append(line)
                        debug('- %s' % line)
            timeout_check(now)
            if change:
                timeout_expire = now + timeout
    else:
        while p.poll() is None:
            now = _now()
            if now > expire:
                wait('.')
                expire = now + 1000
            time.sleep(.1)
            # 检查输入输出
            timeout_check(now)

    _ = p.stdout.readlines()
    if len(_):
        if stdout_buff is not None:
            stdout_buff.extend(_)
        if verbose:
            info(_)
    _ = p.stderr.readlines()
    if len(_):
        if stderr_buff is not None:
            stderr_buff.extend(_)
        error(_)
    if logfile is None:
        if log is not None and os.path.exists(log):
            logfile = open(log, mode='r')
    if logfile is not None:
        _ = logfile.readlines()
        if len(_):
            if stderr_buff is not None:
                stderr_buff.extend(_)
            if verbose:
                info(_)
        logfile.close()
    if p.poll() != 0 and fail:
        raise ConsoleException("执行命令出现错误[%s]" % cmd, stdout_buff, stderr_buff)
    if output:
        return p.poll()


def prepare_dir(path, pwd='.', clean=True):
    path = path.lstrip()
    if not path.startswith('/'):
        path = os.path.join(os.path.abspath(pwd), path)
    if os.path.exists(path):
        if clean:
            shutil.rmtree(path)
        else:
            return path
    os.makedirs(path)
    return path


def print_track():
    traceback.print_exc()


def file_line_iter(path, line_limit=10000, keepLineEnd=False):
    with open(path, mode='rb') as fin:
        for line in fin:
            line = line.decode('utf8', 'ignore').encode('utf')
            if not keepLineEnd:
                yield no_line_end(line)
            else:
                yield line


def grep_file(path, expr, group=0, num=1, found=True):
    return grep(file_line_iter(path), expr, group, num, found, fail_level=1)


def grep(string_or_list, expr, group=0, limit=1, found=True, fail_level=0):
    string = str_iter(string_or_list, fail=False)
    if string is None:
        if found:
            Fail("不能处理的类型", level=1 + fail_level)
        return
    if type(expr) is str:
        expr = re.compile(expr)
    ret = []
    num = limit
    for line in string:
        for item in expr.finditer(line):
            if group == 0:
                s = item.group()
            else:
                s = item.groups()[group - 1]
            ret.append(s.encode())
            num -= 1
            if num == 0:
                break
        if num == 0:
            break
    if len(ret) == 0:
        if found:
            Fail("没有找到[%s]" % expr.pattern, level=1 + fail_level)
        return None
    if limit == 1:
        return ret[0]
    else:
        return ret


def md5(string):
    return hashlib.md5(string).hexdigest()


def try_run(func, exit_when_fail=True):
    def wrapper(*args, **kwargs):
        log = fatal if exit_when_fail else error
        try:
            return func(*args, **kwargs)
        except FailError as e:
            log(e.message)
        except OSError as e:
            log('[%d] %s' % (e.errno, e.strerror))
            print_track()
        except Exception as e:
            if e.message is None or len(e.message) == 0:
                log('出现错误')
            else:
                log(e.message)
            print_track()
            pass
        if exit_when_fail:
            exit(1)

    return wrapper


def file_list(dir_path):
    ret = []
    if dir_path is not None and os.path.exists(dir_path) and os.path.isdir(dir_path):
        for root, _, files in os.walk(dir_path):
            for name in files:
                ret.append(os.path.join(root, name))
    return ret


def ZIP(zip_file, dirs_or_files=None, pwd=None, verbose=False):
    files = []
    t = type(dirs_or_files)
    if t is list:
        pass
    else:
        if t is str:
            dirs_or_files = [dirs_or_files]
        else:
            Fail("不支持的类型[%s]" % t, level=1)
    if pwd is None:
        if len(dirs_or_files) == 1 and os.path.isdir(dirs_or_files[0]):
            pwd = dirs_or_files[0]
        else:
            pwd = os.curdir
    pwd = os.path.abspath(pwd)
    for item in dirs_or_files:
        item = item.lstrip()
        if not item.startswith('/'):
            item = os.path.join(pwd, item)
        if not os.path.exists(item):
            continue
        if os.path.isdir(item):
            files.extend(file_list(item))
        else:
            files.append(item)

    if pwd.endswith(os.path.pathsep):
        pwd += os.path.pathsep
    import zipfile
    zf = zipfile.ZipFile(zip_file, "w", zipfile.zlib.DEFLATED)
    for f in files:
        nf = f.replace(pwd, '')
        zf.write(f, nf)
        if verbose:
            info("<= %s" % nf)
    zf.close()


def write_txt_file(path, content):
    """
    写一个文本文件
    :param path:
    :param content:
    :return:
    """
    with open(path, mode='w') as fout:
        fout.write(content)
